package com.yishuifengxiao.common.tool.utils;

import com.yishuifengxiao.common.tool.exception.UncheckedException;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;

import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.regex.Pattern;

/**
 * <p>
 * 身份证号校验工具
 * </p>
 * <p>
 * 该工具主要是为了校验当前数据是否为一个合法的18位身份证号。
 * 18位身份证号码的含义如下：第7、8、9、10位为出生年份(四位数)，第11、第12位为出生月份，
 * 第13、14位代表出生日期，第17位代表性别，奇数为男，偶数为女。
 * </p>
 * <ol>
 * <li>判断给定的字符串是否为一个合法的18位身份证号</li>
 * <li>从合法的身份证号中提取出当前身份证里的出生日期</li>
 * </ol>
 *
 * @author yishui
 * @version 1.0.0
 * @since 1.0.0
 */
@Slf4j
public final class CertNoUtil {
    /**
     * 18位身份证正则表示
     */
    private static final Pattern REGEX_18_CARD = Pattern.compile("^[1-9]\\d{5}(18|19|([23]\\d))\\d{2}((0[1-9])|" +
            "(10|11|12))(([0-2][1-9])|10|20|30|31)\\d{3}[0-9Xx]$");

    /**
     * 每位加权因子
     */
    private static final int[] POWER = {7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2};

    /**
     * 校验码映射表
     */
    private static final String[] CHECK_CODES = {"1", "0", "X", "9", "8", "7", "6", "5", "4", "3", "2"};

    /**
     * <p>
     * 校验18位身份证号的合法性
     * </p>
     *
     * @param idcard 身份证号
     * @return true表示合法，false不合法
     */
    public static boolean isValid(String idcard) {
        if (StringUtils.isBlank(idcard)) {
            return false;
        }
        idcard = idcard.trim();
        if (!REGEX_18_CARD.matcher(idcard).matches()) {
            return false;
        }

        String idcard17 = idcard.substring(0, 17);
        String idcard18Code = idcard.substring(17, 18);

        if (!isDigital(idcard17)) {
            return false;
        }

        char[] c = idcard17.toCharArray();
        int[] bit = converCharToInt(c);
        int sum17 = getPowerSum(bit);
        String checkCode = getCheckCodeBySum(sum17);

        if (checkCode == null || !idcard18Code.equalsIgnoreCase(checkCode)) {
            return false;
        }

        return true;
    }

    /**
     * <p>
     * 从身份证号里提取出出生日期
     * </p>
     *
     * @param idcard 身份证号
     * @return 出生日期
     */
    public static LocalDate extractBirthday(String idcard) {
        if (!isValid(idcard)) {
            throw new UncheckedException("身份证号格式不正确").setContext(idcard);
        }
        try {
            String dateStr = StringUtils.substring(idcard.trim(), 6, 14);
            return LocalDate.parse(dateStr, DateTimeFormatter.BASIC_ISO_DATE);
        } catch (Exception e) {
            if (log.isInfoEnabled()) {
                log.info("An exception occurred while extracting the birth date from ID number {}. The reason for the" +
                        " " +
                        "exception is {}", idcard.trim(), e.getMessage());
            }

            throw new UncheckedException("身份证号出生日期格式不正确").setContext(idcard);
        }
    }

    /**
     * 判断输入的参数是否为纯数字
     *
     * @param str 输入的参数
     * @return true表示为纯数字
     */
    private static boolean isDigital(String str) {
        return str != null && !str.isEmpty() && str.matches("^[0-9]+$");
    }

    /**
     * 将身份证的每位和对应位的加权因子相乘之后，再得到和值
     *
     * @param bit
     * @return
     */
    private static int getPowerSum(int[] bit) {
        int sum = 0;
        for (int i = 0; i < bit.length; i++) {
            sum += bit[i] * POWER[i];
        }
        return sum;
    }

    /**
     * 将和值与11取模得到余数进行校验码判断
     *
     * @param sum17
     * @return 校验位
     */
    private static String getCheckCodeBySum(int sum17) {
        int remainder = sum17 % 11;
        if (remainder >= 0 && remainder < CHECK_CODES.length) {
            return CHECK_CODES[remainder];
        }
        return null;
    }

    /**
     * 将字符数组转为整型数组
     *
     * @param c
     * @return
     */
    private static int[] converCharToInt(char[] c) {
        int[] a = new int[c.length];
        for (int i = 0; i < c.length; i++) {
            a[i] = c[i] - '0';
        }
        return a;
    }

}
